package game.Engine.Entity;

import java.awt.Image;
import java.awt.Point;
import java.awt.geom.Rectangle2D;
import java.io.Serializable;
import java.util.HashSet;

import com.googlecode.cqengine.attribute.Attribute;
import com.googlecode.cqengine.attribute.SimpleAttribute;

import game.MainController;
import game.display.Animation;


/**
 * This class represents all the entities within our program 
 * Entities include: Aliens, AlienSpawns, Magic, Mouse, Player, PowerUps and Tiles
 * @author Justin
 *
 */
public class Entity implements Comparable<Entity>, Serializable {

   public static enum State {
      NORMAL, DYING, DEAD, SLEEPING
   }

   public static final Attribute<Entity, java.lang.Double> ENTITY_X = new SimpleAttribute<Entity, java.lang.Double>(
         "Entity X") {
      public java.lang.Double getValue(Entity car) {
         return car.getX();
      }
   };
   public static final Attribute<Entity, java.lang.Double> ENTITY_Y = new SimpleAttribute<Entity, java.lang.Double>(
         "Entity Y") {
      public java.lang.Double getValue(Entity car) {
         return car.getY();
      }
   };
   public static final Attribute<Entity, Class> Entity_class = new SimpleAttribute<Entity, Class>(
         "class") {
      public Class<? extends Entity> getValue(Entity car) {
         return car.getClass();
      }
   };
   public static final Attribute<Entity, Boolean> isPowerup = new SimpleAttribute<Entity, Boolean>(
         "Distance") {
      public Boolean getValue(Entity t) {
         return new Boolean(t instanceof PowerUp);
      }
   };
   public static final Attribute<Entity, java.lang.Double> ENTITY_Z = new SimpleAttribute<Entity, java.lang.Double>(
         "Entity Z") {
      public java.lang.Double getValue(Entity car) {
         return car.getZ();
      }
   };

   public static Attribute<Entity, java.lang.Double> distanceFrom(final Entity e) {
      return new SimpleAttribute<Entity, java.lang.Double>("Distance") {
         public java.lang.Double getValue(Entity t) {
            return Math.sqrt(Math.pow((e.getX() - t.getX()), 2)
                  + Math.pow(e.getY() - t.getY(), 2));
         }
      };
   }

   // Static variables
   public static final double SIZEOFPLAYER = 1.0f;
   public static final double SIZEOFALIEN = 1.0f;
   public static final double SIZEOFPOWERUPS = 1.0f;
   public static final double SIZEOFTILES = 1.0f;
   public static final double SIZEOFMAGIC = 1.0f;

   public static final double GRAVITY = 9.8f;

   public static final int DIE_TIME = 1000000000;// How long it takes for the
                                                 // entity
   // to go from dying to dead
   protected Double z;
   protected double x;
   protected double y;
   protected double dx;
   protected double dy;
   protected State state;
   protected long stateTime;
   protected transient Animation gi;
   protected Rectangle2D.Double boundingBox;
   private static int currentZ = 0;

   volatile protected Point position = new Point();

   /**
    *Creates an entity with a location of x and y. The entity also has 
    *an animation of gi.
    */
   public Entity(double x, double y, Animation gi) {
      this.x = x;
      this.y = y;
      this.gi = gi;
      z = new Double(currentZ++);
      target = new Rectangle2D.Double();
   }

   /**
    * Gets the z value of the entity
    * @return z The z value of the entity
    */
   public Double getZ() {
      return z;
   }

   /**
    * Wakes up the entity
    * Changes the state of the entity from SLEEPING to NORMAL
    */
   public void wakeUp() {
      state = State.NORMAL;
      if (state == State.NORMAL && getVelocityX() == 0) {
         setVelocityX(-getMaxSpeed());
      }
   }

   /**
    * Gets the distance between entity a and entity b
    * @param a Entity
    * @param b Entity
    * @return Returns the distance between entity a and entity b
    */
   public static double distanceBetween(Entity a, Entity b) {
      return Math.sqrt(Math.pow(b.getX() - a.getX(), 2)
            + Math.pow(b.getY() - a.getY(), 2));
   }

   /**
    * Gets the state of this Creature. The state is either STATE_NORMAL,
    * STATE_DYING, or STATE_DEAD.
    */
   public State getState() {
      return state;
   }

   /**
    * Sets the state of this Creature to STATE_NORMAL, STATE_DYING, or
    * STATE_DEAD.
    */
   public void setState(State state) {
      if (this.state != state) {
         this.state = state;
         stateTime = 0;
         if (state == State.DYING) {
            setVelocityX(0);
            setVelocityY(0);
         }
      }
   }

   /**
    * Checks if this creature is alive.
    */
   public boolean isAlive() {
      return (state == State.NORMAL);
   }

   /**
    * Gets the maximum speed of this Entity.
    */
   public double getMaxSpeed() {
      return 0;
   }

   /**
    * Gets the x velocity of the entity
    * @return The x velocity of the entity
    */
   public double getVelocityX() {
      return dx;
   }

   /**
    * Sets the x velocity of the entity
    * @param dx The velocity you are setting the entity too
    */
   public void setVelocityX(double dx) {
      if (Math.abs(dx) < .000025) {
         dx = 0;
      }
      this.dx = dx;
   }

   /**
    * Gets the y velocity of the entity
    * @return The y velocity of the entity
    */
   public double getVelocityY() {
      return dy;
   }

   /**
    * Sets the y velocity of the entity
    * @param dy The y velocity of the entity
    */
   public void setVelocityY(double dy) {
      this.dy = dy;
   }

   /**
    * Gets the x location of the entity
    * @return The x location of the entity
    */
   public double getX() {
      return x;
   }

   /**
    * Sets the x location of the entity
    * @param x The location you want to place the entity
    */
   public synchronized void setX(double x) {
      this.x = x;
   }

   /**
    * Gets the y location of the entity
    * @return The y location of the entity
    */
   public double getY() {
      return y;
   }

   /**
    * Sets the y location of the entity
    * @param y The location you want to set the entity too.
    */
   public synchronized void setY(double y) {
      this.y = y;
   }

   /**
    * Gets the bounding box of the entity
    * @return A rectangle that represents the bounding box of the entity
    */
   public Rectangle2D.Double getBoundingBox() {
      if (boundingBox == null) {
         boundingBox = new Rectangle2D.Double(x, y, 1, 1);
      }
      boundingBox.x = x;
      boundingBox.y = y;
      return boundingBox;
   }

   /**
    * Sets the state to dying
    */
   public void die() {
      if (state != State.DEAD) {
         setState(State.DYING);
      }
   }

   /**
    * Gets the current image of the entity
    * @return The current image of the entity
    */
   public Image getCurrentImage() {
      return gi.getImage();
   }

   /**
    * Sets the x and y location of the entity to make the entity move
    * @param timeElapsed The time that elapsed since the last time this method
    * was called
    */
   protected void move(long timeElapsed) {
      x += (timeElapsed * dx) / 1000000000;
      y += (timeElapsed * dy) / 1000000000;
   }

   /**
    * Updates the current animation and updates the state
    * @param timeElapsed The time that elapsed since the last time this method 
    * was called
    */
   public void update(long timeElapsed) {
      gi.update(timeElapsed);
      updateState(timeElapsed);
   }

   /**
    * Updates the state of the entity
    * @param timeElapsed The time that elapsed since the last time this method
    * was called
    */
   private void updateState(long timeElapsed) {
      stateTime += timeElapsed;
      if (state == State.DYING) {
         setVelocityX(0);
         setVelocityY(0);
         gi = getDyingImage();
         if (stateTime > DIE_TIME) {
            setState(State.DEAD);
         }
      }
   }
	
   /**
    * Gets the dying image for the entity
    * @return The dying image for this entity
    */
	public Animation getDyingImage(){
		return gi;
	}

   @Override
   public int compareTo(Entity arg0) {
      return (int) x - (int) arg0.getX();
   }

   /**
    * Makes a clone of this entity
    */
   public Entity clone() {
      return new Entity(x, y, gi);
   }

   private HashSet<Entity> collides = new HashSet<Entity>();
   private Rectangle2D.Double target;
   
   /**
    * Checks to see if the entity can move
    * @param timeElapsed The time that has elapsed since the last time this method
    * was called
    * @param x The x location you want to move to 
    * @param y The y location you want to move to 
    * @return True if the entity can move, false otherwise
    */
   public boolean canMove(long timeElapsed, double x, double y) {
      collides.clear();
      Iterable<Entity> inArea;
      inArea = MainController.gameEngine.runQuery(MainController.gameEngine
            .getNear(this, x));
        target.setRect(x,y,1,1);
      for (Entity e : inArea) {
         Rectangle2D intersection = target.createIntersection(e.getBoundingBox());
         double area = intersection.getWidth() * intersection.getHeight();
         if (area > 0 && intersection.getWidth() > 0) {
            collides.add(e);
         }
      }
      return collides.size() == 0;
   }

   /**
    * This method will adjust the course of the entity and move the entity to that
    * new location
    * @param timeElapsed The amount of time that has elapsed since the method was
    * last called
    */
   public void adjustCourseAndMove(long timeElapsed) {
      double originalXm = Math.abs((timeElapsed * dx) / 1000000000);
      double originalYm = Math.abs((timeElapsed * dy) / 1000000000);
      double minYDist = Math.abs((timeElapsed * dy) / 1000000000);
      double minXDist = Math.abs((timeElapsed * dx) / 1000000000);
      int signX = (int) (dx / Math.abs(dx));
      int signY = (int) (dy / Math.abs(dy));
      for (Entity e : collides) {
         boolean collidesBottom = false;
         if (!(MainController.gameEngine.currentMap.entities[(int) e.getX()][(int) e
               .getY() - 1] instanceof Tile)) {
            collidesBottom = true;
         }
         boolean collidesTop = false;
         if (!(MainController.gameEngine.currentMap.entities[(int) e.getX()][(int) e
               .getY() + 1] instanceof Tile)) {
            collidesTop = true;
         }
         double yDist = Math.abs(e.y - getY());
         if (e.x - getX() >= 0) {
            if (e.y - getY() >= 0) {
               if (yDist >= 1) {
                  if (collidesBottom) {
                     double yDist2 = Math.abs(e.y - getY() - 1);
                     if (minYDist > yDist2) {
                        minYDist = yDist2;
                     }
                     // System.out.println("right down hit bottom");
                     if (this instanceof Player) {
                        ((Player) this).setOnGround(true);
                     }
                  } else {
                     double xDist = e.x - getX() - 1;
                     if (xDist < 0) {
                        signX = -signX;
                     }
                     // System.out
                     // .println("right down hit side collidedBottom");
                     if (minXDist > Math.abs(xDist)) {
                        minXDist = Math.abs(xDist);
                     }
                  }
               } else {
                  double xDist = e.x - getX() - 1;
                  if (xDist < 0) {
                     signX = -signX;
                  }
                  // System.out.println("right down hit side");
                  dx = 0;
                  if (minXDist > Math.abs(xDist)) {
                     minXDist = Math.abs(xDist);
                  }
               }
            } else {
               if (yDist >= 1) {
                  if (collidesTop) {
                     double yDist2 = Math.abs(e.y - getY() + 1);
                     // System.out.println("up right hit top");
                     if (minYDist > yDist2) {
                        minYDist = yDist2;
                     }
                  } else {
                     double xDist = e.x - getX() - 1;
                     // right = true;
                     // System.out.println(" right up hit side collidedtop");
                     if (xDist < 0)
                        signX = -signX;
                     if (minXDist > Math.abs(xDist)) {
                        minXDist = Math.abs(xDist);
                     }
                  }
               } else {
                  double xDist = e.x - getX() - 1;
                  // right = true;
                  // System.out.println(" right up hit side");
                  if (xDist < 0)
                     signX = -signX;
                  dx = 0;
                  if (minXDist > Math.abs(xDist)) {
                     minXDist = Math.abs(xDist);
                  }
               }
            }
         } else {
            if (e.y - getY() >= 0) {
               if (yDist >= 1) {
                  if (collidesBottom) {
                     double yDist2 = Math.abs(e.y - getY() - 1);
                     // System.out.println("left down hit bottom");
                     if (minYDist > yDist2) {
                        minYDist = yDist2;
                     }
                     if (this instanceof Player) {
                        ((Player) this).setOnGround(true);
                     }
                  } else {
                     double xDist = Math.abs(e.x - getX() + 1);
                     // System.out.println("left down hit side");
                     if (minXDist > xDist) {
                        minXDist = xDist;
                     }
                  }
               } else {
                  double xDist = Math.abs(e.x - getX() + 1);
                  // System.out.println("left down hit side");
                  if (minXDist > xDist) {
                     minXDist = xDist;
                  }
               }
            } else {
               if (yDist >= 1) {
                  if (collidesTop) {
                     double yDist2 = Math.abs(e.y - getY() + 1);
                     // System.out.println("left up hit top");
                     if (minYDist > yDist2) {
                        minYDist = yDist2;
                     }
                     y += .005;
                  } else {
                     double xDist = Math.abs(e.x - getX() + 1);
                     // System.out.println("left up hit side");
                     if (minXDist > xDist) {
                        minXDist = xDist;
                     }
                  }
               } else {
                  double xDist = Math.abs(e.x - getX() + 1);
                  // System.out.println("left up hit side");
                  if (minXDist > xDist) {
                     minXDist = xDist;
                  }
               }
            }
         }
      }
      x += signX * minXDist;
      y += signY * minYDist;
      if (minXDist < originalXm) {
         dx = 0;
      }
      if (minYDist < originalYm) {
         dy = 0;
      }
   }

   /**
    * Handles the collision
    * Overwritten in subclasses
    * @param e
    */
   public void handleCollision(Entity e) {
      // Overwrite in subclasses
   }

   /**
    * Moves vertically
    * @param timeElapsed The amount of time that has elapsed since this method was
    * last called
    */
   public void moveVert(long timeElapsed) {
      x += (timeElapsed * dx) / 1000000000;
   }

   /**
    * Moves horizontal
    * @param timeElapsed The amount of time that has elapsed since this method was
    * last called
    */
   public void moveHoriz(long timeElapsed) {
      y += (timeElapsed * dy) / 1000000000;
   }

   /**
    * Checks to see if the entity was hit by Magic m
    * @param m Magic
    */
   public void hitBy(Magic m) {};
}
